// userstat.go - Stupidly complicated package to regulate frequency and number 
//               of active connections per remote IP. Tracks other user
//               statistics to counter abuse of UI's interactive features.

package userstat

import (
    "../ageMap"
    "config"
    "sync"
    "sync/atomic"
    "time"
)

var userMap *ageMap.AgeMap

// This structure holds user-specific data, access to which is guarded under an
// interface which serializes access through channels into a go routine which
// serves these out. Ideally when this grows it will save a lot of concurrency
// headaches.
type UserData struct {
    ReportsMade int
}

// Individual user entry. 
type userRecord struct {
    remoteAddr string

    Info *UserData // General per-user info storage described above.

    // This is cache-related data. The mutex does not apply to any of the
    // fields in this struct, it's simply an informational lock to notify if
    // the user is already being served. A_ReqsActive and LastReq are handled 
    // outside the channel interface with basic atomic primitives for
    // for performance's sake. Probably not worth the trouble, really; likely,
    // it's just premature optimization. But I feel like it. Vroom, vroom.
    LastReq      time.Time
    A_ReqsActive *int64 // *ATOMIC* # of pending requests open.
    ReqMtx       *sync.Mutex

    // Info for AgeMap to play with.
    Generated time.Time
    ValidFor  time.Duration
}

func (u *userRecord) Key() string             { return u.remoteAddr }
func (u *userRecord) Created() time.Time      { return u.Generated }
func (u *userRecord) Lifetime() time.Duration { return u.ValidFor }

// Communication to UserData server goroutine.
var commands chan *internalCommand

// Access secondary user information through server goroutine.
func FetchInfo(addr string) UserData {
    out := make(chan UserData)
    commands <- &internalCommand{Op: GET, Addr: addr, Out: out}
    return <-out
}

func PutInfo(addr string, info UserData) {
    commands <- &internalCommand{Op: PUT, Addr: addr, Input: info}
}

// Defining internal commands to UserData server.
type operation uint8

const (
    GET operation = iota
    PUT
)

type internalCommand struct {
    Op    operation
    Addr  string
    Input UserData
    Out   chan<- UserData
}

// Spawned at init time to loop forever and handle access to secondary user
// data.
func serveInfo(in <-chan *internalCommand) {
    for {
        cmd := <-in
        var user *userRecord
        item := userMap.Fetch(cmd.Addr)
        if item == nil {
            user = newUser(cmd.Addr)
            userMap.Store(user)
        } else {
            user = item.(*userRecord)
        }

        switch cmd.Op {
        case GET:
            cmd.Out <- *user.Info
        case PUT:
            user.Info = &cmd.Input
        default:
            panic("Invalid commannd to execop: " + string(cmd.Op))
        }
    }
}

// Create new user record and clear out any old ones.
func newUser(addr string) *userRecord {
    u := new(userRecord)
    u.Info = new(UserData)
    u.remoteAddr = addr
    u.ReqMtx = new(sync.Mutex)
    u.Generated = time.Now()
    u.ValidFor = config.Defaults.UserDataHoldTime
    u.A_ReqsActive = new(int64)
    userMap.Prune()
    return u
}

// Wait until mutex is free, then return true. If user has exceeded Req
// limit, just return false immediately.
func LockUser(addr string) bool {
    var user *userRecord
    item := userMap.Fetch(addr)
    if item == nil {
        user = newUser(addr)
        userMap.Store(user)
    } else {
        user = item.(*userRecord)
    }

    // I know this looks stupid. I don't care if it goes over by a couple. But
    // on some architectures, updating a single integer is not atomic, which
    // would throw this completely out of whack. Hence the calls you see here.
    if atomic.LoadInt64(user.A_ReqsActive) >
        int64(config.Defaults.MaxUserRequests) {
        return false
    }
    atomic.AddInt64(user.A_ReqsActive, 1)

    user.ReqMtx.Lock()
    elapsed := time.Now().Sub(user.LastReq)
    if elapsed < config.Defaults.MinReqWaitTime {
        time.Sleep(config.Defaults.MinReqWaitTime - elapsed)
    }
    return true
}

func UnlockUser(addr string) {
    var user *userRecord
    item := userMap.Fetch(addr)
    if item == nil {
        // It's possible they may have gotten pruned during a really
        // long Req, or user_data_hold_time was set ridiculously low.
        user = newUser(addr)
        userMap.Store(user)
        user.LastReq = time.Now()
    } else {
        user = item.(*userRecord)
        user.LastReq = time.Now()
        atomic.AddInt64(user.A_ReqsActive, -1)
        user.ReqMtx.Unlock()
    }
}

// Initialize userMap and start secondary userdata server.
func init() {
    userMap = ageMap.New()
    commands = make(chan *internalCommand, 10) // arbitrary buffer size.
    go serveInfo(commands)
}
